/*
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package com.biglybt.ui.swt.views.table.impl;

import java.text.Collator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;

import com.biglybt.ui.common.table.TableColumnCore;
import com.biglybt.ui.common.table.TableColumnSortObject;
import com.biglybt.ui.common.table.TableRowCore;
import com.biglybt.ui.common.table.TableView;
import com.biglybt.ui.swt.debug.ObfuscateCellText;
import com.biglybt.ui.swt.utils.SWTRunnable;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.widgets.Display;

import com.biglybt.core.internat.MessageText;
import com.biglybt.core.logging.LogEvent;
import com.biglybt.core.logging.LogIDs;
import com.biglybt.core.logging.Logger;
import com.biglybt.core.util.*;
import com.biglybt.pif.ui.Graphic;
import com.biglybt.pif.ui.tables.*;
import com.biglybt.ui.swt.Utils;
import com.biglybt.ui.swt.mainwindow.Colors;
import com.biglybt.ui.swt.pif.UISWTGraphic;
import com.biglybt.ui.swt.pifimpl.UISWTGraphicImpl;
import com.biglybt.ui.swt.views.table.*;

import com.biglybt.ui.swt.utils.ColorCache;

public abstract class TableCellSWTBase
	implements TableCellSWT
{
	private static final LogIDs LOGID = LogIDs.GUI;

	private static AEMonitor this_mon = new AEMonitor("TableCell");

	/**
	 * Plugins read this to see if their datasource has changed.
	 * {@link #invalidate()} will clear this flag, causing the cell to set its ui again
	 */
	protected static final int FLAG_VALID = 1;

	/**
	 * Indicates if the sort value is also the text displayed.  We can optimize.
	 */
	protected static final int FLAG_SORTVALUEISTEXT = 2;

	/**
	 * Indicates if the tooltip is autogenerated
	 */
	protected static final int FLAG_TOOLTIPISAUTO = 4;

	/**
	 * For refreshing, this flag manages whether the row is actually up to date.
	 *
	 * We don't update any visuals while the row isn't visible.  But, validility
	 * does get set to true so that the cell isn't forced to refresh every
	 * cycle when not visible.  (We can't just never call refresh when the row
	 * is not visible, as refresh also sets the sort value)
	 *
	 * When the row does become visible, we have to invalidate the row so
	 * that the row will set its visuals again (this time, actually
	 * updating a viewable object).
	 */
	protected static final int FLAG_UPTODATE = 8;

	protected static final int FLAG_DISPOSED = 16;

	/**
	 * Cell has been invalidated, it must refresh on next cycle
	 */
	protected static final int FLAG_MUSTREFRESH = 32;

	/**
	 * If any visuals change between 2 refreshes, this flag gets set
	 */
	public static final int FLAG_VISUALLY_CHANGED_SINCE_REFRESH = 64;

	private static final boolean DEBUGONLYZERO = false;

	private static final boolean DEBUG_FLAGS = false;

	private int flags;

	protected TableRowSWTBase tableRowSWT;

	protected TableColumnSWTBase 	tableColumnSWT;
	protected TableColumnCore 		tableColumnCore;

	private byte tooltipErrLoopCount;

	public boolean bDebug = false;

	protected ArrayList<TableCellRefreshListener> refreshListeners;

	private ArrayList<TableCellDisposeListener> disposeListeners;

	private ArrayList<TableCellToolTipListener> tooltipListeners;

	private ArrayList<TableCellMouseListener> cellMouseListeners;
	
	private ArrayList<TableCellMenuListener> cellMenuListeners;

	private ArrayList<TableCellMouseMoveListener> cellMouseMoveListeners;

	private ArrayList<TableCellVisibilityListener> cellVisibilityListeners;

	protected ArrayList<TableCellSWTPaintListener> cellSWTPaintListeners;

	private ArrayList<TableCellClipboardListener> cellClipboardListeners;

	protected Comparable sortValue;

	private byte restartRefresh = 0;

	private boolean bInRefreshAsync = false;

	private byte refreshErrLoopCount;

	private byte loopFactor;

	// TODO: private
	protected static int MAX_REFRESHES = 10;

	private static int MAX_REFRESHES_WITHIN_MS = 100;

	private boolean bInRefresh = false;

	private long lastRefresh;

	// TODO: Private
	protected int numFastRefreshes;

	private Object oToolTip;

	private Object defaultToolTip;

	private int textAlpha = 255;

	private boolean doFillCell = false;

	private int iCursorID = SWT.CURSOR_ARROW;

	private boolean mouseOver;

	private Image icon;

	private Graphic graphic = null;

	private Map<Object,Object>	userData;

	public TableCellSWTBase(TableRowSWTBase row, TableColumnSWTBase column) {
		flags = FLAG_SORTVALUEISTEXT;
		tableRowSWT = row;
		tableColumnSWT = column;
		
		tableColumnCore = column.getColumnCore();
		
		tooltipErrLoopCount = 0;
		refreshErrLoopCount = 0;
		loopFactor = 0;
	}

	protected void
	constructionComplete()
	{
			// this needs to be done after the subclass has had time to initialise itself

		if (tableColumnCore != null && tableColumnCore.getType() == TableColumnCore.TYPE_GRAPHIC) {
			setMarginHeight(1);
			setMarginWidth(1);
		}
	}

	protected abstract void
	constructionCompleter();

	@Override
	public void addRefreshListener(TableCellRefreshListener listener) {
		try {
			this_mon.enter();

			if (refreshListeners == null)
				refreshListeners = new ArrayList(1);

			if (bDebug) {
				debug("addRefreshListener; count=" + refreshListeners.size());
			}
			refreshListeners.add(listener);

		} finally {
			this_mon.exit();
		}
	}

	@Override
	public void removeRefreshListener(TableCellRefreshListener listener) {
		try {
			this_mon.enter();

			if (refreshListeners == null)
				return;

			refreshListeners.remove(listener);
		} finally {

			this_mon.exit();
		}
	}

	@Override
	public void addDisposeListener(TableCellDisposeListener listener) {
		try {
			this_mon.enter();

			if (disposeListeners == null) {
				disposeListeners = new ArrayList(1);
			}
			disposeListeners.add(listener);
		} finally {

			this_mon.exit();
		}
	}

	@Override
	public void removeDisposeListener(TableCellDisposeListener listener) {
		try {
			this_mon.enter();

			if (disposeListeners == null)
				return;

			disposeListeners.remove(listener);

		} finally {

			this_mon.exit();
		}
	}

	@Override
	public void addToolTipListener(TableCellToolTipListener listener) {
		try {
			this_mon.enter();

			if (tooltipListeners == null) {
				tooltipListeners = new ArrayList(1);
			}
			tooltipListeners.add(listener);

		} finally {
			this_mon.exit();
		}
	}

	@Override
	public void removeToolTipListener(TableCellToolTipListener listener) {
		try {
			this_mon.enter();

			if (tooltipListeners == null)
				return;

			tooltipListeners.remove(listener);
		} finally {

			this_mon.exit();
		}
	}

	@Override
	public void addMouseListener(TableCellMouseListener listener) {
		try {
			this_mon.enter();

			if (cellMouseListeners == null)
				cellMouseListeners = new ArrayList(1);

			cellMouseListeners.add(listener);

		} finally {
			this_mon.exit();
		}
	}

	@Override
	public void removeMouseListener(TableCellMouseListener listener) {
		try {
			this_mon.enter();

			if (cellMouseListeners == null)
				return;

			cellMouseListeners.remove(listener);

		} finally {
			this_mon.exit();
		}
	}

	@Override
	public void addMenuListener(TableCellMenuListener listener) {
		try {
			this_mon.enter();

			if (cellMenuListeners == null)
				cellMenuListeners = new ArrayList(1);

			cellMenuListeners.add(listener);

		} finally {
			this_mon.exit();
		}
	}

	@Override
	public void removeMenuListener(TableCellMenuListener listener) {
		try {
			this_mon.enter();

			if (cellMenuListeners == null)
				return;

			cellMenuListeners.remove(listener);

		} finally {
			this_mon.exit();
		}
	}
	
	public void addMouseMoveListener(TableCellMouseMoveListener listener) {
		try {
			this_mon.enter();

			if (cellMouseMoveListeners == null)
				cellMouseMoveListeners = new ArrayList(1);

			cellMouseMoveListeners.add(listener);

		} finally {
			this_mon.exit();
		}
	}

	public void removeMouseMoveListener(TableCellMouseMoveListener listener) {
		try {
			this_mon.enter();

			if (cellMouseMoveListeners == null)
				return;

			cellMouseMoveListeners.remove(listener);

		} finally {
			this_mon.exit();
		}
	}

	public void addVisibilityListener(TableCellVisibilityListener listener) {
		try {
			this_mon.enter();

			if (cellVisibilityListeners == null)
				cellVisibilityListeners = new ArrayList(1);

			cellVisibilityListeners.add(listener);

		} finally {
			this_mon.exit();
		}
	}

	public void removeVisibilityListener(TableCellVisibilityListener listener) {
		try {
			this_mon.enter();

			if (cellVisibilityListeners == null)
				return;

			cellVisibilityListeners.remove(listener);

		} finally {
			this_mon.exit();
		}
	}

	/**
	 * @param listenerObject
	 *
	 * @since 3.1.1.1
	 */
	private void addSWTPaintListener(TableCellSWTPaintListener listener) {
		try {
			this_mon.enter();

			if (cellSWTPaintListeners == null)
				cellSWTPaintListeners = new ArrayList(1);

			cellSWTPaintListeners.add(listener);

		} finally {
			this_mon.exit();
		}
	}

	public void invokeSWTPaintListeners(GC gc) {
		if (tableColumnCore != null) {
			Object[] swtPaintListeners = tableColumnCore.getCellOtherListeners("SWTPaint");
			if (swtPaintListeners != null) {
				for (int i = 0; i < swtPaintListeners.length; i++) {
					try {
						TableCellSWTPaintListener l = (TableCellSWTPaintListener) swtPaintListeners[i];

						l.cellPaint(gc, this);

					} catch (Throwable e) {
						Debug.printStackTrace(e);
					}
				}
			}
		}

		if (cellSWTPaintListeners == null) {
			return;
		}

		for (int i = 0; i < cellSWTPaintListeners.size(); i++) {
			try {
				TableCellSWTPaintListener l = (TableCellSWTPaintListener) (cellSWTPaintListeners.get(i));

				l.cellPaint(gc, this);

			} catch (Throwable e) {
				Debug.printStackTrace(e);
			}
		}
	}

	private void addCellClipboardListener(TableCellClipboardListener listener) {
		try {
			this_mon.enter();

			if (cellClipboardListeners == null)
				cellClipboardListeners = new ArrayList<>(1);

			cellClipboardListeners.add(listener);

		} finally {
			this_mon.exit();
		}
	}

	@Override
	public String getClipboardText() {
		if (isDisposed()) {
			return "";
		}
		String text = tableColumnCore.getClipboardText(this);
		if (text != null) {
			return text;
		}

		try {
			this_mon.enter();

			if (cellClipboardListeners != null) {
				for (TableCellClipboardListener l : cellClipboardListeners) {
					try {
						text = l.getClipboardText(this);
					} catch (Exception e) {
						Debug.out(e);
					}
					if (text != null) {
						break;
					}
				}
			}
		} finally {
			this_mon.exit();
		}
		if (text == null) {
			text = this.getText();
		}
		return text;
	}

	@Override
	public void addListeners(Object listenerObject) {
		if (listenerObject instanceof TableCellDisposeListener) {
			addDisposeListener((TableCellDisposeListener) listenerObject);
		}

		if (listenerObject instanceof TableCellRefreshListener)
			addRefreshListener((TableCellRefreshListener) listenerObject);

		if (listenerObject instanceof TableCellToolTipListener)
			addToolTipListener((TableCellToolTipListener) listenerObject);

		if (listenerObject instanceof TableCellMouseMoveListener) {
			addMouseMoveListener((TableCellMouseMoveListener) listenerObject);
		}

		if (listenerObject instanceof TableCellMouseListener) {
			addMouseListener((TableCellMouseListener) listenerObject);
		}

		if (listenerObject instanceof TableCellMenuListener) {
			addMenuListener((TableCellMenuListener) listenerObject);
		}
		
		if (listenerObject instanceof TableCellVisibilityListener)
			addVisibilityListener((TableCellVisibilityListener) listenerObject);

		if (listenerObject instanceof TableCellSWTPaintListener)
			addSWTPaintListener((TableCellSWTPaintListener) listenerObject);

		if (listenerObject instanceof TableCellClipboardListener)
			addCellClipboardListener((TableCellClipboardListener) listenerObject);
	}

	@Override
	public void invokeToolTipListeners(int type) {
		if (tableColumnCore == null)
			return;

		tableColumnCore.invokeCellToolTipListeners(this, type);

		if (tooltipListeners == null || tooltipErrLoopCount > 2)
			return;

		int iErrCount = tableColumnCore.getConsecutiveErrCount();
		if (iErrCount > 10)
			return;

		try {
			if (type == TOOLTIPLISTENER_HOVER) {
				for (int i = 0; i < tooltipListeners.size(); i++)
					((TableCellToolTipListener) (tooltipListeners.get(i))).cellHover(this);
			} else {
				for (int i = 0; i < tooltipListeners.size(); i++)
					((TableCellToolTipListener) (tooltipListeners.get(i))).cellHoverComplete(this);
			}
			tooltipErrLoopCount = 0;
		} catch (Throwable e) {
			tooltipErrLoopCount++;
			tableColumnCore.setConsecutiveErrCount(++iErrCount);
			pluginError(e);
			if (tooltipErrLoopCount > 2)
				Logger.log(new LogEvent(LOGID, LogEvent.LT_ERROR,
						"TableCell's tooltip will not be refreshed anymore this session."));
		}
	}

	@Override
	public void invokeMouseListeners(TableCellMouseEvent event) {
		ArrayList listeners = event.eventType == TableCellMouseEvent.EVENT_MOUSEMOVE
				? cellMouseMoveListeners : cellMouseListeners;
		if (listeners == null)
			return;

		if (event.cell != null && event.row == null) {
			event.row = event.cell.getTableRow();
		}

		for (int i = 0; i < listeners.size(); i++) {
			try {
				TableCellMouseListener l = (TableCellMouseListener) (listeners.get(i));

				l.cellMouseTrigger(event);

			} catch (Throwable e) {
				Debug.printStackTrace(e);
			}
		}
	}

	@Override
	public void 
	invokeMenuListeners(
		TableCellMenuEvent event ) 
	{
		
		ArrayList<TableCellMenuListener> listeners = cellMenuListeners;
		
		if ( listeners == null ){
			
			return;
		}
		
		for ( TableCellMenuListener listener: listeners ){
			
			if ( event.skipCoreFunctionality ){
				
				break;
			}
			
			try{
				listener.menuEventOccurred(event);

			}catch ( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}

	
	@Override
	public void invokeVisibilityListeners(int visibility,
	                                      boolean invokeColumnListeners) {
		TableColumnCore tc = tableColumnCore;

		if (invokeColumnListeners && tc != null) {
			tc.invokeCellVisibilityListeners(this, visibility);
		}

		if (cellVisibilityListeners == null)
			return;

		for (int i = 0; i < cellVisibilityListeners.size(); i++) {
			try {
				TableCellVisibilityListener l = cellVisibilityListeners.get(i);

				l.cellVisibilityChanged(this, visibility);

			} catch (Throwable e) {
				Debug.printStackTrace(e);
			}
		}
	}

	@Override
	public void dispose() {
		if ( isDisposed()){
			// parg added this check at same time as removing the isDisposed check in getDataSource
			// in case there is some recursive disposal occuring on a dispose-listener
			Debug.out( "Double disposal!" );
			return;
		}
		setFlag(FLAG_DISPOSED);

		TableColumnCore tc = tableColumnCore;

		if (tc != null) {
			tc.invokeCellDisposeListeners(this);
		}

		if (disposeListeners != null) {
			try {
				for (Iterator iter = disposeListeners.iterator(); iter.hasNext();) {
					TableCellDisposeListener listener = (TableCellDisposeListener) iter.next();
					listener.dispose(this);
				}
				disposeListeners = null;
			} catch (Throwable e) {
				pluginError(e);
			}
		}

		refreshListeners = null;
		tableColumnCore = null;
		tableColumnSWT = null;
		tableRowSWT = null;
		sortValue = null;
	}

	public void debug(final String s) {
		if (DEBUGONLYZERO && tableColumnCore.getPosition() != 0) {
			return;
		}
		Utils.execSWTThread(new AERunnable() {
			@Override
			public void runSupport() {
				if (tableRowSWT == null) {
					System.out.println(SystemTime.getCurrentTime() + ": c"
							+ (tableColumnCore == null ? null : tableColumnCore.getPosition()) + ";F=" + flagToText(flags, false)
							+ ";" + s);

				} else {
					System.out.println(SystemTime.getCurrentTime() + ": r"
							+ tableRowSWT.getIndex() + "c"
							+ (tableColumnCore == null ? null : tableColumnCore.getPosition())
							+ ";r.v?" + ((tableRowSWT.isVisible() ? "Y" : "N")) + "F="
							+ flagToText(flags, false) + ";" + s);
				}
			}
		}, true);
	}

	protected void pluginError(Throwable e) {
		if (tableColumnCore != null) {
  		String sTitleLanguageKey = tableColumnCore.getTitleLanguageKey();

  		String sPosition = tableColumnCore.getPosition() + " ("
  				+ MessageText.getString(sTitleLanguageKey) + ")";
  		Logger.log(new LogEvent(LOGID, "Table Cell Plugin for Column #" + sPosition
  				+ " generated an exception ", e));
		} else {
  		Logger.log(new LogEvent(LOGID, "Table Cell Plugin generated an exception ", e));
		}
	}

	protected void pluginError(String s) {
		String sPosition = "r" + tableRowSWT.getIndex() + "c"
				+ tableColumnCore.getPosition();
		Logger.log(new LogEvent(LOGID, LogEvent.LT_ERROR,
				"Table Cell Plugin for Column #" + sPosition + ":" + s + "\n  "
						+ Debug.getStackTrace(true, true)));
	}

	@Override
	public boolean refresh() {
		return refresh(true);
	}

	// @see TableCellCore#refresh(boolean)
	@Override
	public boolean refresh(boolean bDoGraphics) {
		boolean isRowShown;
		if (tableRowSWT != null) {
			TableView view = tableRowSWT.getView();
			isRowShown = view.isRowVisible(tableRowSWT);
		} else {
			isRowShown = true;
		}
		boolean isCellShown = isRowShown && isShown();
		return refresh(bDoGraphics, isRowShown, isCellShown);
	}

	// @see TableCellCore#refresh(boolean, boolean)
	@Override
	public boolean refresh(boolean bDoGraphics, boolean bRowVisible) {
		boolean isCellShown = bRowVisible && isShown();
		return refresh(bDoGraphics, bRowVisible, isCellShown);
	}

	// @see TableCellCore#refresh(boolean, boolean, boolean)
	@Override
	public boolean refresh(boolean bDoGraphics, boolean bRowVisible,
	                       boolean bCellVisible) {
		//  	if (Utils.isThisThreadSWT()) {
		//  		System.out.println("ONSWT: " + Debug.getCompressedStackTrace());
		//  	}
		TableColumnCore	tc = tableColumnCore;

		if (tc == null) {
			return false;
		}
		boolean ret = getVisuallyChangedSinceRefresh();
		// don't clear flag -- since anyone can call refresh(), only the code that
		// actually refreshes the display should clear the flag.
		//clearFlag(FLAG_VISUALLY_CHANGED_SINCE_REFRESH);

		int iErrCount = 0;
		if (refreshErrLoopCount > 2) {
			return ret;
		}

		iErrCount = tc.getConsecutiveErrCount();
		if (iErrCount > 10) {
			refreshErrLoopCount = 3;
			return ret;
		}

		if (bInRefresh) {
			// Skip a Refresh call when being called from within refresh.
			// This could happen on virtual tables where SetData calls us again, or
			// if we ever introduce plugins to refresh.
			if (bDebug)
				debug("Calling Refresh from Refresh :) Skipping.");
			return ret;
		}
		try {
			bInRefresh = true;
			if (ret) {
				long now = SystemTime.getCurrentTime();
				if (now - lastRefresh < MAX_REFRESHES_WITHIN_MS) {
					numFastRefreshes++;
					if (numFastRefreshes >= MAX_REFRESHES) {
						if ((numFastRefreshes % MAX_REFRESHES) == 0) {
							pluginError("this plugin is crazy. tried to refresh "
									+ numFastRefreshes + " times in " + (now - lastRefresh)
									+ "ms");
						}
						return ret;
					}
				} else {
					numFastRefreshes = 0;
					lastRefresh = now;
				}
			}

			// See bIsUpToDate variable comments
			if (bCellVisible && !isUpToDate()) {
				if (bDebug)
					debug("Setting Invalid because visible & not up to date");
				clearFlag(FLAG_VALID);
				setFlag(FLAG_UPTODATE);
			} else if (!bCellVisible && isUpToDate()) {
				if (bDebug)
					debug("Setting not up to date because cell not visible "
							+ Debug.getCompressedStackTrace());
				clearFlag(FLAG_UPTODATE);
			}

			if (bDebug) {
				debug("Cell Valid?" + hasFlag(FLAG_VALID) + "; Visible?"
						+ tableRowSWT.isVisible() + "/" + isShown());
			}
			int iInterval = tc.getRefreshInterval();
			if (iInterval == TableColumnCore.INTERVAL_INVALID_ONLY
					&& !hasFlag(FLAG_MUSTREFRESH | FLAG_VALID)
					&& hasFlag(FLAG_SORTVALUEISTEXT) && sortValue != null
					&& tc.getType() == TableColumnCore.TYPE_TEXT_ONLY) {
				if (bCellVisible) {
					if (bDebug)
						debug("fast refresh: setText");
					ret = setText((String) sortValue);
					setFlag(FLAG_VALID);
				}
			} else if ((iInterval == TableColumnCore.INTERVAL_LIVE
					|| (iInterval == TableColumnCore.INTERVAL_GRAPHIC && bDoGraphics)
					|| (iInterval > 0 && (loopFactor % iInterval) == 0)
					|| !hasFlag(FLAG_VALID) || hasFlag(FLAG_MUSTREFRESH))) {
				boolean bWasValid = isValid();

				ret = hasFlag(FLAG_MUSTREFRESH);
				if (ret) {
					clearFlag(FLAG_MUSTREFRESH);
				}

				if (bDebug) {
					debug("invoke refresh; wasValid? " + bWasValid);
				}

				long lTimeStart = Constants.isCVSVersion()
						? SystemTime.getMonotonousTime() : 0;
				tc.invokeCellRefreshListeners(this, !bCellVisible);
				if (refreshListeners != null) {
					for (TableCellRefreshListener l : refreshListeners) {
						if (l instanceof TableCellLightRefreshListener) {
							((TableCellLightRefreshListener) l).refresh(this, !bCellVisible);
						} else {
							l.refresh(this);
						}
					}
				}
				if (Constants.isCVSVersion()) {
					long lTimeEnd = SystemTime.getMonotonousTime();
					tc.addRefreshTime(lTimeEnd - lTimeStart);
				}

				// Change to valid only if we weren't valid before the listener calls
				// This is in case the listeners set valid to false when it was true
				if (!bWasValid && !hasFlag(FLAG_MUSTREFRESH)) {
					setFlag(FLAG_VALID);
				}
			} else {
				if (bCellVisible && bDebug) {
					debug("Skipped refresh; Interval=" + iInterval);
				}
			}
			loopFactor++;
			refreshErrLoopCount = 0;
			if (iErrCount > 0)
				tc.setConsecutiveErrCount(0);

			// has changed if visually changed or "must refresh"
			ret |= getVisuallyChangedSinceRefresh();
			if (bDebug)
				debug("refresh done; visual change? " + ret + ";"
						+ Debug.getCompressedStackTrace());
		} catch (Throwable e) {
			refreshErrLoopCount++;
			if (tc != null) {
				tc.setConsecutiveErrCount(++iErrCount);
			}
			pluginError(e);
			if (refreshErrLoopCount > 2)
				Logger.log(new LogEvent(LOGID, LogEvent.LT_ERROR,
						"TableCell will not be refreshed anymore this session."));
		} finally {
			bInRefresh = false;
		}

		return ret;
	}

	@Override
	public boolean setSortValue(Comparable valueToSort) {
		if ( tableColumnCore == null ){
			return( false );
		}
		if (!tableColumnCore.isSortValueLive()) {
			// objects that can't change aren't live
			if (!(valueToSort instanceof Number) && !(valueToSort instanceof String)
					&& !(valueToSort instanceof TableColumnSortObject)) {
				tableColumnCore.setSortValueLive(true);
			}
		}
		return _setSortValue(valueToSort);
	}

	private boolean _setSortValue(Comparable valueToSort) {
		if (isDisposed()) {
			return false;
		}

		if (sortValue == valueToSort)
			return false;

		if (hasFlag(FLAG_SORTVALUEISTEXT)) {
			clearFlag(FLAG_SORTVALUEISTEXT);
			if (sortValue instanceof String)
				// Make sure text is actually in the cell (it may not have been if
				// cell wasn't created at the time of setting)
				setText((String) sortValue);
		}

		if ((valueToSort instanceof String) && (sortValue instanceof String)
				&& sortValue.equals(valueToSort)) {
			return false;
		}

		if ((valueToSort instanceof Number) && (sortValue instanceof Number)
				&& sortValue.equals(valueToSort)) {
			return false;
		}

		if (bDebug)
			debug("Setting SortValue to "
					+ ((valueToSort == null) ? "null" : valueToSort.getClass().getName()));

		tableColumnCore.setLastSortValueChange(SystemTime.getCurrentTime());
		sortValue = valueToSort;

		// Columns with SWT Paint Listeners usually rely on a repaint whenever the
		// sort value changes
		if (cellSWTPaintListeners != null
				|| tableColumnCore.hasCellOtherListeners("SWTPaint")) {
    	setFlag(FLAG_VISUALLY_CHANGED_SINCE_REFRESH);
			//redraw();
		}

		return true;
	}

	@Override
	public boolean setSortValue(long valueToSort) {
		if (isDisposed()) {
			return false;
		}

		if ((sortValue instanceof Long)
				&& ((Long) sortValue).longValue() == valueToSort)
			return false;

		return _setSortValue(Long.valueOf(valueToSort));
	}

	@Override
	public boolean setSortValue(float valueToSort) {
		if (isDisposed()) {
			return false;
		}

		if (sortValue instanceof Float
				&& ((Float) sortValue).floatValue() == valueToSort)
			return false;

		return _setSortValue(new Float(valueToSort));
	}

	@Override
	public Comparable getSortValue() {
		return sortValue;
	}

	@Override
	public boolean isValid() {
		// Called often.. inline faster
		return (flags & FLAG_VALID) != 0;
		//return hasFlag(FLAG_VALID);
	}

	@Override
	public boolean isDisposed() {
		return (flags & FLAG_DISPOSED) != 0;
	}

	public boolean hasFlag(int flag) {
		return (flags & flag) != 0;
	}

	public void setFlag(int flag) {
		if (DEBUG_FLAGS && (flags & flag) != flag) {
			debug("SET FLAG " + flagToText((~flags) & flag, true) + " via "
					+ Debug.getStackTrace(true, true, 1, 7));
		}
		flags |= flag;
	}

	public void clearFlag(int flag) {
		if (DEBUG_FLAGS && (flags & flag) != 0) {
			debug("CLEAR FLAG " + flagToText(flags & flag, true) + " via "
					+ Debug.getStackTrace(true, true, 1, 7));
		}
		flags &= ~flag;
	}

	/**
	 * If a plugin in trying to invalidate a cell, then clear the sort value
	 * too.
	 */
	@Override
	public void invalidate() {
		if (isDisposed()) {
			return;
		}

		invalidate(true);
	}

	/* (non-Javadoc)
	 * @see TableCellCore#invalidate(boolean)
	 */
	@Override
	public void invalidate(boolean bMustRefresh) {
		//if (bInRefresh && Utils.isThisThreadSWT()) {
		//	System.out.println("Invalidating when in refresh via " + Debug.getCompressedStackTrace());
		//}
		if ((flags & (FLAG_VALID | FLAG_VISUALLY_CHANGED_SINCE_REFRESH)) == FLAG_VISUALLY_CHANGED_SINCE_REFRESH) { //!hasFlag(FLAG_VALID)
			if (bMustRefresh) {
				if ((flags & FLAG_MUSTREFRESH) != 0) {
					return;
				}
			} else {
				if (DEBUG_FLAGS) {
					debug("ALREADY FLAGGED for invalidate via " + Debug.getCompressedStackTrace(7));
				}
				return;
			}
		}
		clearFlag(FLAG_VALID);

		if (bDebug)
			debug("Invalidate Cell;" + bMustRefresh);

		if (bMustRefresh) {
			setFlag(FLAG_MUSTREFRESH | FLAG_VISUALLY_CHANGED_SINCE_REFRESH);
		} else {
			setFlag(FLAG_VISUALLY_CHANGED_SINCE_REFRESH);
		}
	}

	// @see TableCellCore#refreshAsync()
	@Override
	public void refreshAsync() {
		if (bInRefreshAsync) {
			//System.out.println(System.currentTimeMillis() + "] SKIP " + restartRefresh);
			if (restartRefresh < Byte.MAX_VALUE) {
				restartRefresh++;
			}
			return;
		}
		bInRefreshAsync = true;

		AERunnable runnable = new AERunnable() {
			@Override
			public void runSupport() {
				//System.out.println(System.currentTimeMillis() + "] REFRESH!");
				restartRefresh = 0;
				refresh(true);
				bInRefreshAsync = false;
				//System.out.println(System.currentTimeMillis() + "] REFRESH OUT!");
				if (restartRefresh > 0) {
					refreshAsync();
				}
			}
		};
		Utils.execSWTThreadLater(25, runnable);
	}

	@Override
	public void setUpToDate(boolean upToDate) {
		if (bDebug)
			debug("set up to date to " + upToDate);
		if (upToDate) {
			setFlag(FLAG_UPTODATE);
		} else {
			clearFlag(FLAG_UPTODATE);
		}
	}

	@Override
	public boolean isUpToDate() {
		return hasFlag(FLAG_UPTODATE);
	}

	@Override
	public boolean getVisuallyChangedSinceRefresh() {
		return hasFlag(FLAG_VISUALLY_CHANGED_SINCE_REFRESH);
	}

	public void clearVisuallyChangedSinceRefresh() {
		clearFlag(FLAG_VISUALLY_CHANGED_SINCE_REFRESH);
	}

	/** Compare our sortValue to the specified object.  Assumes the object
	 * is TableCellSWTBase (safe assumption)
	 */
	@Override
	public int compareTo(Object o) {
		try {
			Comparable ourSortValue = getSortValue();
			Comparable otherSortValue = ((TableCellSWTBase) o).getSortValue();
			if (ourSortValue instanceof String && otherSortValue instanceof String) {
				// Collator.getInstance cache's Collator object, so this is relatively
				// fast.  However, storing it as static somewhere might give a small
				// performance boost.  If such an approach is take, ensure that the static
				// variable is updated the user chooses an different language.
				Collator collator = Collator.getInstance(Locale.getDefault());
				return collator.compare(ourSortValue, otherSortValue);
			}
			try {
				return ourSortValue.compareTo(otherSortValue);
			} catch (ClassCastException e) {
				// It's possible that a row was created, but not refreshed yet.
				// In that case, one sortValue will be String, and the other will be
				// a comparable object that the plugin defined.  Those two sortValues
				// may not be compatable (for good reason!), so just skip it.
			}
		} catch (Exception e) {
			System.out.println("Could not compare cells");
			Debug.printStackTrace(e);
		}
		return 0;
	}

	@Override
	public boolean needsPainting() {
		if (isDisposed()) {
			return false;
		}

		if (cellSWTPaintListeners != null
				|| tableColumnCore.hasCellOtherListeners("SWTPaint")) {
			return true;
		}

		return getGraphic() != null;
	}

	@Override
	public boolean setText(String text) {
		if (isDisposed()) {
			return false;
		}

		if (text == null)
			text = "";
		boolean bChanged = false;

		if (hasFlag(FLAG_SORTVALUEISTEXT) && !text.equals(sortValue)) {
			bChanged = true;
			sortValue = text;
			tableColumnCore.setLastSortValueChange(SystemTime.getCurrentTime());
			if (bDebug)
				debug("Setting SortValue to text;");
		}

		// Slower than setText(..)!
		//  	if (isInvisibleAndCanRefresh()) {
		//  		if (bDebug) {
		//  			debug("setText ignored: invisible");
		//  		}
		//  		return false;
		//  	}

		if (uiSetText(text) && !hasFlag(FLAG_SORTVALUEISTEXT))
			bChanged = true;

		if (bDebug) {
			debug("setText (" + bChanged + ") : " + text);
		}

		if (bChanged) {
			setFlag(FLAG_VISUALLY_CHANGED_SINCE_REFRESH);
		}

		boolean do_auto = tableColumnCore == null ? false : tableColumnCore.doesAutoTooltip();

		// If we were using auto tooltips (and we aren't any more), then
		// clear up previously set tooltips.
		if (!do_auto) {
			if (hasFlag(FLAG_TOOLTIPISAUTO)) {
				this.oToolTip = null;
				clearFlag(FLAG_TOOLTIPISAUTO);
			}
		}

		else {
			this.oToolTip = text;
			setFlag(FLAG_TOOLTIPISAUTO);
		}

		return bChanged;
	}

	@Override
	public void setToolTip(Object tooltip) {
		oToolTip = tooltip;

		if (tooltip == null) {
			setFlag(FLAG_TOOLTIPISAUTO);
		} else {
			clearFlag(FLAG_TOOLTIPISAUTO);
		}
	}

	@Override
	public Object getToolTip() {
		return ( TableTooltips.tooltips_disabled && !tableColumnCore.doesAutoTooltip())?null:oToolTip;
	}

	@Override
	public Object getDefaultToolTip() {
		return ( TableTooltips.tooltips_disabled && !tableColumnCore.doesAutoTooltip())?null:defaultToolTip;
	}

	@Override
	public void setDefaultToolTip(Object tt) {
		defaultToolTip = tt;
	}

	public abstract boolean uiSetText(String text);

  @Override
  public void doPaint(GC gc) {
  	//This sometimes causes a infinite loop if the listener invalidates
  	//the drawing area
  	//if ((!hasFlag(FLAG_UPTODATE) || !hasFlag(FLAG_VALID)) && !bInRefresh && !bInRefreshAsync
		//		&& (refreshListeners != null || tableColumn.hasCellRefreshListener())) {
  	//	if (bDebug) {
  	//		debug("doPaint: invoke refresh");
  	//	}
  	//	refresh(true);
  	//}

		if (bDebug) {
			debug("doPaint up2date:" + hasFlag(FLAG_UPTODATE) + ";v:" + hasFlag(FLAG_VALID) + ";rl=" + refreshListeners);
		}

		invokeSWTPaintListeners(gc);
  }

  @Override
  public int getTextAlpha() {
		return textAlpha;
	}

	@Override
	public void setTextAlpha(int textOpacity) {
		this.textAlpha = textOpacity;
	}


	// @see com.biglybt.ui.swt.views.table.TableCellSWT#getTableRowSWT()
	@Override
	public TableRowSWT getTableRowSWT() {
		if (tableRowSWT instanceof TableRowSWT) {
			return (TableRowSWT)tableRowSWT;
		}
		return null;
	}

  @Override
  public TableRowCore getTableRowCore() {
    return tableRowSWT;
  }

  public TableColumnSWT
  getTableColumnSWT()
  {
	  return( tableColumnSWT );
  }
  
  private String flagToText(int flag, boolean onlySet) {
  	StringBuilder sb = new StringBuilder();
  	sb.append((flag & FLAG_DISPOSED) > 0 ? 'D' : onlySet ? ' ' : 'd');
  	sb.append((flag & FLAG_MUSTREFRESH) > 0 ? 'M' : onlySet ? ' ' : 'm');
  	sb.append((flag & FLAG_SORTVALUEISTEXT) > 0 ? 'S' : onlySet ? ' ' : 's');
  	sb.append((flag & FLAG_TOOLTIPISAUTO) > 0 ? 'T' : onlySet ? ' ' : 't');
  	sb.append((flag & FLAG_UPTODATE) > 0 ? 'U' : onlySet ? ' ' : 'u');
  	sb.append((flag & FLAG_VALID) > 0 ? 'V' : onlySet ? ' ' : 'v');
  	sb.append((flag & FLAG_VISUALLY_CHANGED_SINCE_REFRESH) > 0 ? "VC" : onlySet ? " " : "vc");
  	return sb.toString();
  }

	public abstract int getWidthRaw();


	/* (non-Javadoc)
	 * @see com.biglybt.pif.ui.tables.TableCell#setFillCell(boolean)
	 */
	@Override
	public void setFillCell(boolean doFillCell) {
		this.doFillCell = doFillCell;
	}

	public boolean getFillCell() {
		return doFillCell;
	}


	@Override
	public TableColumnCore getTableColumnCore() {
		return tableColumnCore;
	}

	@Override
	public boolean setCursorID(int cursorID) {
		if (iCursorID == cursorID) {
			return false;
		}
		iCursorID = cursorID;
		return true;
	}


	@Override
	public int getCursorID() {
		return iCursorID;
	}

	@Override
	public void setMouseOver(boolean b) {
		mouseOver = b;
	}

	@Override
	public boolean isMouseOver() {
		if (tableRowSWT != null && !tableRowSWT.isVisible()) {
			// XXX: Should trigger event and set mouseOver false?
			return false;
		}
		return mouseOver;
	}

  @Override
  public boolean setIcon(Image img) {
  	if (isInvisibleAndCanRefresh())
  		return false;

  	icon = img;

    graphic = null;
    setFlag(FLAG_VISUALLY_CHANGED_SINCE_REFRESH);
    return true;
  }

  @Override
  public Image getIcon() {
  	return icon;
  }

  // @see com.biglybt.ui.swt.views.table.TableCellSWT#setGraphic(org.eclipse.swt.graphics.Image)
  @Override
  public boolean setGraphic(Image img) {
  	return setGraphic(new UISWTGraphicImpl(img));
  }

  // @see com.biglybt.pif.ui.tables.TableCell#setGraphic(com.biglybt.pif.ui.Graphic)
  @Override
  public boolean setGraphic(Graphic img) {
  	if (img != null && isDisposed()) {
			return false;
  	}

		if (tableColumnCore == null
				|| tableColumnCore.getType() != TableColumnCore.TYPE_GRAPHIC) {
      return false;
    }

    if (img == graphic && numFastRefreshes >= MAX_REFRESHES) {
    	pluginError("TableCellImpl::setGraphic to same Graphic object. "
					+ "Forcing refresh.");
    }

    boolean changed = (img == graphic || (img != null && !img.equals(graphic)) || (graphic != null && !graphic.equals(img)));

    graphic = img;

    if (changed) {
    	setFlag(FLAG_VISUALLY_CHANGED_SINCE_REFRESH);
    	if (tableRowSWT == null || tableRowSWT.isVisible()) {
    		if ( !tableRowSWT.isInPaintItem()){
    			redraw();
    		}
	    }
    }

    return changed;
  }

  @Override
  public Graphic getGraphic() {
  	return graphic;
  }

  @Override
  public Image getGraphicSWT() {
		return (graphic instanceof UISWTGraphic)
				? ((UISWTGraphic) graphic).getImage() : null;
  }

	public boolean isInvisibleAndCanRefresh() {
  	return !isDisposed() && !isShown()
				&& (refreshListeners != null || tableColumnCore.hasCellRefreshListener());
	}

  @Override
  public int[] getBackground() {
		Color color = getBackgroundSWT();

		if (color == null) {
			return null;
		}

		return new int[] {
			color.getRed(),
			color.getGreen(),
			color.getBlue()
		};
	}

  // @see com.biglybt.pif.ui.tables.TableCell#getForeground()
  @Override
  public int[] getForeground() {
		Color color = getForegroundSWT();

		if (color == null) {
			return new int[3];
		}

		return new int[] { color.getRed(), color.getGreen(), color.getBlue()
		};
	}

	/* (non-Javadoc)
	 * @see com.biglybt.pif.ui.tables.TableCell#setForeground(int, int, int)
	 */
	@Override
	public boolean setForeground(int red, int green, int blue) {
		// Don't need to set when not visible
		if (isInvisibleAndCanRefresh()) {
			return false;
		}

		if (red < 0 || green < 0 || blue < 0) {
			return setForeground((Color) null);
		}
		return setForeground(new RGB(red, green, blue));
	}

	private boolean setForeground(final RGB rgb) {
		Color colorFG = getForegroundSWT();
		boolean changed = colorFG == null || colorFG.isDisposed()
				|| !colorFG.getRGB().equals(rgb);
		if (changed) {
			Utils.execSWTThread(new SWTRunnable() {
				@Override
				public void runWithDisplay(Display display) {
					setForeground(ColorCache.getColor(Display.getCurrent(), rgb));
				}
			});
		}
		return changed;
	}

	// @see com.biglybt.pif.ui.tables.TableCell#setForeground(int[])
	@Override
	public boolean setForeground(int[] rgb) {
		if (rgb == null || rgb.length < 3) {
			return setForeground((Color) null);
		}
		return setForeground(rgb[0], rgb[1], rgb[2]);
	}

  @Override
  public boolean setForegroundToErrorColor() {
	  return setForeground(Colors.colorError);
  }

	@Override
	public int[] getMouseOffset() {
		Point ofs = ((TableViewSWT) tableRowSWT.getView()).getTableCellMouseOffset(this);
		return ofs == null ? null : new int[] { ofs.x, ofs.y };
	}

	@Override
	public String getObfuscatedText() {
		if (isDisposed()) {
			return null;
		}
		if (tableColumnCore.isObfuscated()) {
			if (tableColumnCore instanceof ObfuscateCellText) {
				return ((ObfuscateCellText)tableColumnCore).getObfuscatedText(this);
			}

			return "";
		}
		return null;
	}

	@Override
	public boolean useSimpleSortValue() {
		if (tableRowSWT == null) {
			return false;
		}
		TableView view = tableRowSWT.getView();
		if (view == null) {
			return false;
		}
		// Since cells often use sort value to determine if they need to update
		// their text, using simple sort value can prevent changes from being
		// displayed
		// Cells of sort columns are always invalid (afaik), so they always update properly
		// Cells not in sort must not be simple to trigger new text
		return view.getSortColumnCount() > 1 && view.hasSortColumn(tableColumnCore);
	}
	
	@Override
	public Object getData(Object key){
		synchronized( this ){
			if ( userData == null ){
				return( null );
			}else{
				return( userData.get( key ));
			}
		}
	}
	
	@Override
	public void setData(Object key, Object data){
		synchronized( this ){
			if ( userData == null ){
				if ( data == null ){
					return;
				}else{
					userData = new HashMap<>();
				}
			}
			if ( data == null ){
				userData.remove( key );
				if ( userData.isEmpty()){
					userData = null;
				}
			}else{
				userData.put(key, data);
			}
		}
	}
}
